<!doctype html><html lang=zh-hans>
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<meta http-equiv=x-ua-compatible content="IE=edge">
<meta name=author content="小李刀刀">
<meta name=description content="Spiral 默认的 Web 项目框架 已经包含了配置好的路由组件。但如果你需要在自己构建的项目或其它类型的项目中安装路由组件，可以使用以下命令： $ composer require spiral/router 然后在激活它的引导程序： [ //... Spiral\Bootloader\Http\RouterBootloader::class, ] 默认配置 Web 项目框架的默认配置允许你使用 /<controller>/<action> 的模式访问 App\Controller 命名空间下的控制器方法。如果你不喜欢这种模式，可以根据下面的文档">
<meta name=theme-color content="#3f51b5">
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/academicons/1.8.6/css/academicons.min.css integrity="sha256-uFVgMKfistnJAfoCUQigIl+JfUaP47GrRKjf6CTPVmw=" crossorigin=anonymous>
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.0-1/css/all.min.css integrity="sha256-4w9DunooKSr3MFXHXWyFER38WmPdm361bQS/2KUWZbU=" crossorigin=anonymous>
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css integrity="sha256-Vzbj7sDDS/woiFS3uNKo8eIuni59rjyNGtXfstRzStA=" crossorigin=anonymous>
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.1/styles/tomorrow-night.min.css crossorigin=anonymous title=hl-light>
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.1/styles/tomorrow-night.min.css crossorigin=anonymous title=hl-dark disabled>
<script src=https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.1.2/lazysizes.min.js integrity="sha256-Md1qLToewPeKjfAHU1zyPwOutccPAm5tahnaw7Osw0A=" crossorigin=anonymous async></script>
<link rel=stylesheet href="https://fonts.font.im/css?family=Source+Code+Pro:300,400,500,600,700&subset=latin-ext&display=swap">
<link rel=stylesheet href=/css/academic.css>
<script>(function(b,d,e,a,g){b[a]=b[a]||[],b[a].push({'gtm.start':(new Date).getTime(),event:'gtm.js'});var f=d.getElementsByTagName(e)[0],c=d.createElement(e),h=a!='dataLayer'?'&l='+a:'';c.async=!0,c.src='https://www.googletagmanager.com/gtm.js?id='+g+h,f.parentNode.insertBefore(c,f)})(window,document,'script','dataLayer','GTM-5KZH8N7')</script>
<link rel=manifest href=/index.webmanifest>
<link rel=icon type=image/png href=/images/icon_hu0b7a4cb9992c9ac0e91bd28ffd38dd00_9727_32x32_fill_lanczos_center_3.png>
<link rel=apple-touch-icon type=image/png href=/images/icon_hu0b7a4cb9992c9ac0e91bd28ffd38dd00_9727_192x192_fill_lanczos_center_3.png>
<link rel=canonical href=https://studyspiral.cn/docs/http/routing/>
<meta property="twitter:card" content="summary">
<meta property="og:site_name" content="Spiral中文网">
<meta property="og:url" content="https://studyspiral.cn/docs/http/routing/">
<meta property="og:title" content="路由 | Spiral中文网">
<meta property="og:description" content="Spiral 默认的 Web 项目框架 已经包含了配置好的路由组件。但如果你需要在自己构建的项目或其它类型的项目中安装路由组件，可以使用以下命令： $ composer require spiral/router 然后在激活它的引导程序： [ //... Spiral\Bootloader\Http\RouterBootloader::class, ] 默认配置 Web 项目框架的默认配置允许你使用 /<controller>/<action> 的模式访问 App\Controller 命名空间下的控制器方法。如果你不喜欢这种模式，可以根据下面的文档"><meta property="og:image" content="https://studyspiral.cn/images/logo.svg">
<meta property="twitter:image" content="https://studyspiral.cn/images/logo.svg"><meta property="og:locale" content="zh-Hans">
<meta property="article:published_time" content="2020-04-12T23:21:35+08:00">
<meta property="article:modified_time" content="2020-08-28T15:38:18+08:00">
<script src=https://cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.1.1/cookieconsent.min.js integrity="sha256-5VhCqFam2Cn+yjw61zbBNrbHVJ6SRydPeKopYlngbiQ=" crossorigin=anonymous></script>
<link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.1.1/cookieconsent.min.css integrity="sha256-zQ0LblD/Af8vOppw18+2anxsuaz3pWYyVWi+bTvTH8Q=" crossorigin=anonymous>
<script>window.addEventListener("load",function(){window.cookieconsent.initialise({palette:{popup:{background:"#3f51b5",text:"#fff"},button:{background:"#fff",text:"#3f51b5"}},theme:"classic",content:{message:"本网站使用cookies来确保您在本网站上获得最佳体验。",dismiss:"知道了!",link:"了解更多",href:"https://www.cookiesandyou.com"}})})</script>
<title>路由 | Spiral中文网</title>
</head>
<body id=top data-spy=scroll data-offset=70 data-target=#TableOfContents>
<aside class=search-results id=search>
<div class=container>
<section class=search-header>
<div class="row no-gutters justify-content-between mb-3">
<div class=col-6>
<h1>搜索</h1>
</div>
<div class="col-6 col-search-close">
<a class=js-search href=#><i class="fas fa-times-circle text-muted" aria-hidden=true></i></a>
</div>
</div>
<div id=search-box>
<input name=q id=search-query placeholder=搜索... autocapitalize=off autocomplete=off autocorrect=off spellcheck=false type=search>
</div>
</section>
<section class=section-search-results>
<div id=search-hits>
</div>
</section>
</div>
</aside>
<nav class="navbar navbar-expand-lg navbar-light compensate-for-scrollbar" id=navbar-main>
<div class=container>
<div class="d-none d-lg-inline-flex">
<a class=navbar-brand href=/><img src=/images/logo.svg alt=Spiral中文网>Spiral中文网</a>
</div>
<button type=button class=navbar-toggler data-toggle=collapse data-target=#navbar-content aria-controls=navbar aria-expanded=false aria-label=切换导航>
<span><i class="fas fa-bars"></i></span>
</button>
<div class="navbar-brand-mobile-wrapper d-inline-flex d-lg-none">
<a class=navbar-brand href=/><img src=/images/logo.svg alt=Spiral中文网>Spiral中文网</a>
</div>
<div class="navbar-collapse main-menu-item collapse justify-content-end" id=navbar-content>
<ul class="navbar-nav d-md-inline-flex">
<li class=nav-item>
<a class=nav-link href=/docs/basics/quick-start/><span>教程</span></a>
</li>
<li class=nav-item>
<a class="nav-link active" href=/docs/><span>文档</span></a>
</li>
<li class=nav-item>
<a class=nav-link href=/post/><span>文章</span></a>
</li>
</ul>
</div>
<ul class="nav-icons navbar-nav flex-row ml-auto d-flex pl-md-2">
<li class=nav-item>
<a class="nav-link js-search" href=#><i class="fas fa-search" aria-hidden=true></i></a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid docs">
<div class="row flex-xl-nowrap">
<div class="col-12 col-md-3 col-xl-2 docs-sidebar">
<form class="docs-search d-flex align-items-center">
<button class="btn docs-toggle d-md-none p-0 mr-3" type=button data-toggle=collapse data-target=#docs-nav aria-controls=docs-nav aria-expanded=false aria-label="Toggle section navigation">
<span><i class="fas fa-bars"></i></span>
</button>
<input name=q type=search class=form-control placeholder=搜索... autocomplete=off>
</form>
<nav class="collapse docs-links" id=docs-nav>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/extension/dotenv/>Dotenv</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/>总览</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse show">
<li>
<a href=/docs/>Spiral介绍</a>
</li>
<li>
<a href=/docs/about/contributing/>贡献指引</a>
</li>
<li>
<a href=/docs/about/semver/>版本说明</a>
</li>
<li>
<a href=/docs/about/license/>授权协议</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/http/psr-15/>定制 PSR-15 处理器</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/start/install/>快速开始</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/start/install/>安装指引</a>
</li>
<li>
<a href=/docs/start/workers/>应用工作进程</a>
</li>
<li>
<a href=/docs/start/structure/>应用程序结构</a>
</li>
<li>
<a href=/docs/start/configuration/>默认配置</a>
</li>
<li>
<a href=/docs/start/commands/>控制台命令</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/basics/quick-start/>入门基础</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/basics/quick-start/>上手指南</a>
</li>
<li>
<a href=/docs/basics/scaffolding/>脚手架</a>
</li>
<li>
<a href=/docs/basics/prototype/>原型开发辅助</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/framework/design/>核心框架</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/framework/design/>设计理念</a>
</li>
<li>
<a href=/docs/framework/application-server/>应用服务器</a>
</li>
<li>
<a href=/docs/framework/config/>配置对象</a>
</li>
<li>
<a href=/docs/framework/kernel/>内核与环境</a>
</li>
<li>
<a href=/docs/framework/container/>容器</a>
</li>
<li>
<a href=/docs/framework/bootloaders/>引导程序</a>
</li>
<li>
<a href=/docs/framework/scopes/>IoC 作用域</a>
</li>
<li>
<a href=/docs/framework/memory/>静态高速缓存</a>
</li>
<li>
<a href=/docs/framework/finalizers/>终结器</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/cookbook/annotated-routes/>速查手册</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/cookbook/annotated-routes/>注解式路由</a>
</li>
<li>
<a href=/docs/cookbook/injector/>容器注入器</a>
</li>
<li>
<a href=/docs/cookbook/domain-core/>领域内核、控制器</a>
</li>
<li>
<a href=/docs/cookbook/golang-library/>Golang服务集成</a>
</li>
<li>
<a href=/docs/cookbook/custom-dispatcher/>自定义调度器</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/component/files/>常用组件</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/component/files/>文件和目录</a>
</li>
<li>
<a href=/docs/component/reactor/>代码生成</a>
</li>
<li>
<a href=/docs/component/tokenizer/>静态分析工具</a>
</li>
<li>
<a href=/docs/component/metrics/>应用指标</a>
</li>
<li>
<a href=/docs/component/data-grid/>数据网格</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/console/configuration/>控制台</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/console/configuration/>安装和配置</a>
</li>
<li>
<a href=/docs/console/commands/>用户命令</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/http/configuration/>HTTP</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse show">
<li>
<a href=/docs/http/configuration/>安装配置</a>
</li>
<li>
<a href=/docs/http/lifecycle/>请求生命周期</a>
</li>
<li>
<a href=/docs/http/request-response/>请求和响应</a>
</li>
<li class=active>
<a href=/docs/http/routing/>路由</a>
</li>
<li>
<a href=/docs/http/errors/>错误页面</a>
</li>
<li>
<a href=/docs/http/middleware/>中间件</a>
</li>
<li>
<a href=/docs/http/golang/>Golang 中间件</a>
</li>
<li>
<a href=/docs/http/cookies/>Cookies</a>
</li>
<li>
<a href=/docs/http/session/>Session</a>
</li>
<li>
<a href=/docs/http/csrf/>CSRF 防护</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/security/encrypter/>安全</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/security/encrypter/>数据加密</a>
</li>
<li>
<a href=/docs/security/validation/>数据验证</a>
</li>
<li>
<a href=/docs/security/rbac/>基于角色的权限控制</a>
</li>
<li>
<a href=/docs/security/authentication/>用户认证</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/filters/configuration/>请求过滤</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/filters/configuration/>安装和配置</a>
</li>
<li>
<a href=/docs/filters/filter/>过滤器</a>
</li>
<li>
<a href=/docs/filters/composite/>复合过滤器</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/database/configuration/>数据库</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/database/configuration/>安装与配置</a>
</li>
<li>
<a href=/docs/database/access/>访问数据</a>
</li>
<li>
<a href=/docs/database/isolation/>逻辑隔离</a>
</li>
<li>
<a href=/docs/database/query-builders/>查询构造器</a>
</li>
<li>
<a href=/docs/database/transactions/>Transactions</a>
</li>
<li>
<a href=/docs/database/introspection/>Schema Introspection</a>
</li>
<li>
<a href=/docs/database/declaration/>Declaration</a>
</li>
<li>
<a href=/docs/database/migrations/>Migrations</a>
</li>
<li>
<a href=/docs/database/errata/>Errata</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/cycle/configuration/>Cycle ORM</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/cycle/configuration/>Configuration</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/queue/configuration/>队列任务</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/queue/configuration/>Configuration</a>
</li>
<li>
<a href=/docs/queue/scraper/>网站爬虫</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/views/configuration/>视图</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/views/configuration/>Configuration</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link href=/docs/stempler/configuration/>Stempler 模板</a>
<ul class="nav docs-sidenav docs-sidenav-sub collapse">
<li>
<a href=/docs/stempler/configuration/>Configuration</a>
</li>
<li>
<a href=/docs/stempler/directives/>Directives</a>
</li>
</ul>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link>国际化</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link>GRPC</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link>事件广播</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link>调试及性能</a>
</div>
<div class=docs-toc-item>
<a class=docs-toc-link>扩展</a>
</div>
</nav>
</div>
<div class="d-none d-xl-block col-xl-2 docs-toc">
<ul class="nav toc-top">
<li><a href=# id=back_to_top class=docs-toc-title>在本页</a></li>
</ul>
<nav id=TableOfContents>
<ul>
<li><a href=#默认配置>默认配置</a></li>
<li><a href=#路由配置>路由配置</a></li>
<li><a href=#闭包处理器>闭包处理器</a></li>
<li><a href=#匹配模式和参数>匹配模式和参数</a>
<ul>
<li><a href=#匹配主机名>匹配主机名</a></li>
<li><a href=#路由的不变性>路由的不变性</a></li>
<li><a href=#动词>动词</a></li>
<li><a href=#中间件>中间件</a></li>
</ul>
</li>
<li><a href=#多路由配置>多路由配置</a></li>
<li><a href=#默认路由>默认路由</a></li>
<li><a href=#路由目标控制器方法>路由目标（控制器方法）</a>
<ul>
<li><a href=#指向控制器方法>指向控制器方法</a></li>
<li><a href=#方法通配符>方法通配符</a></li>
<li><a href=#指向控制器>指向控制器</a></li>
<li><a href=#指向命名空间>指向命名空间</a></li>
<li><a href=#指向控制器组>指向控制器组</a></li>
</ul>
</li>
<li><a href=#restful>RESTful</a>
<ul>
<li><a href=#跨路由共享目标>跨路由共享目标</a></li>
</ul>
</li>
<li><a href=#url-生成>URL 生成</a></li>
</ul>
</nav>
</div>
<main class="col-12 col-md-9 col-xl-8 py-md-3 pl-md-5 docs-content" role=main>
<article class=article>
<div class="alert alert-warning" role=alert>
官方文档中文版翻译工作仍在进行中，欢迎 <a href=/post/join-translation/>参与翻译</a>。
</div>
<div class=docs-article-container>
<h1>路由</h1>
<div class=article-style>
<p>Spiral 默认的
<a href=https://github.com/spiral/app target=_blank rel=noopener>Web 项目框架</a> 已经包含了配置好的路由组件。但如果你需要在自己构建的项目或其它类型的项目中安装路由组件，可以使用以下命令：</p>
<pre><code class=language-bash>$ composer require spiral/router
</code></pre>
<p>然后在激活它的引导程序：</p>
<pre><code class=language-php>[
    //...
    Spiral\Bootloader\Http\RouterBootloader::class,
]
</code></pre>
<h2 id=默认配置>默认配置</h2>
<p>Web 项目框架的默认配置允许你使用 <code>/&lt;controller>/&lt;action></code> 的模式访问 <code>App\Controller</code> 命名空间下的控制器方法。如果你不喜欢这种模式，可以根据下面的文档来修改这一行为。</p>
<blockquote>
<p>控制器的类名必须有 <code>Controller</code> 后缀。</p>
</blockquote>
<h2 id=路由配置>路由配置</h2>
<p>默认情况下路由组件不需要任何额外配置就能正常使用。但你当然可以在引导程序中通过 <code>Spiral\Router\RouterInterface</code> 来创建新的路由。作为示例，我们来创建一个最基础的处理 <code>/</code> 的路由：</p>
<pre><code class=language-php>namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute(
            'home', // 路由名称
            new Route(
                '/', // 匹配模式
                function () { return 'hello world'; } // 路由处理器
            )
        );
    }
}
</code></pre>
<blockquote>
<p>Route 类接受 <code>Psr\Http\Server\RequestHandlerInterface</code> 类型的参数，不管是闭包函数、可调用的类或者 <code>Spiral\Router\TargetInterface</code> 的实现都可以。如果你希望路由处理器按需创建，也可以传入类或者类名字符串。</p>
</blockquote>
<h2 id=闭包处理器>闭包处理器</h2>
<p>可以传入一个闭包函数作为路由处理器，这种情况下，该函数会收到两个参数：<code>Psr\Http\Message\ServerRequestInterface</code> 和 <code>Psr\Http\Message\ResponseInterface</code>.</p>
<pre><code class=language-php>router-&gt;setRoute('home', new Route(
    '/&lt;name&gt;',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        $response-&gt;getBody()-&gt;write(&quot;hello world&quot;);

        return $response;
    }
));
</code></pre>
<h2 id=匹配模式和参数>匹配模式和参数</h2>
<p>通过路由匹配模式，可以指定任意数量的必须和可选参数。这些参数会通过 <code>ServerRequestInterface</code> 的 <code>matches</code> 属性传递给路由处理器。</p>
<blockquote>
<p>在请求过滤器（Request Filter）中可以通过 <code>attribute:matches.&lt;key></code> 来取回路由参数的值。</p>
</blockquote>
<p>在定义路由参数时，可以采用 <code>&lt;parameter_name:pattern></code> 的格式来限制参数格式，这里的 <code>pattern</code> 是正则表达式。如果不想对参数格式进行限制，可以省略 <code>pattern</code>，只使用 <code>&lt;parameter_name></code>，在这种情况（省略格式限制）下，参数会匹配 <code>[^\/]+</code>.</p>
<p>比如简单地添加一个名为 <code>name</code> 的参数：</p>
<pre><code class=language-php>namespace App\Bootloader;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute('home', new Route(
            '/&lt;name&gt;',
            function (ServerRequestInterface $request, ResponseInterface $response) {
                return $request-&gt;getAttribute('route')-&gt;getMatches(); // 返回 JSON ['name' =&gt; '']
            }
        ));
    }
}
</code></pre>
<p>如果希望路由中的参数是可选的，只需用 <code>[]</code> 把路由匹配模式对应的部分（包括参数）包裹起来，例如：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '/[&lt;name&gt;]',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<blockquote>
<p>上面的例子可以匹配 <code>/</code>，没有传递参数时，name 参数的值是 <code>null</code>.</p>
</blockquote>
<p>可以指定任意数量的参数，并使他们的中的一部分或全部作为可选。比如，要定义一个类似 <code>/group/user</code> 的路由，其中 <code>/user</code> 是可选的：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '/&lt;group&gt;[/&lt;user&gt;]',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<p>在定义路由的时候，可以通过 <code>Route</code> 构造函数的第三个参数为路由参数指定默认值：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '/&lt;group&gt;[/&lt;user&gt;]',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    },
    [
        'user' =&gt; 'default' // 为 user 参数指定默认值 `default`
    ]
));
</code></pre>
<p>最后再讲一下怎么用 <code>&lt;parameter:pattern></code> 规定参数的格式：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '/user/&lt;id:\d+&gt;',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<blockquote>
<p>上面的例子只有在 <code>id</code> 是数字时才能匹配到这个路由规则。</p>
</blockquote>
<p>除了使用正则表达式以外，也可以给参数指定多个预设值：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '/do/&lt;action:login|logout&gt;',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<blockquote>
<p>这个路由规则只匹配 <code>/do/login</code> 和 <code>/do/logout</code>.</p>
</blockquote>
<h3 id=匹配主机名>匹配主机名</h3>
<p>还可以指定路由只匹配特定的域名或者子域名，只要给匹配模式增加 <code>//</code> 前缀：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '//&lt;host&gt;/',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<p>匹配子域名：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '//&lt;sub&gt;.domain.com/',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<p>可以混合使用主机和路径的匹配规则：</p>
<pre><code class=language-php>$router-&gt;setRoute('home', new Route(
    '//&lt;sub&gt;.domain.com/[&lt;action&gt;]',
    function (ServerRequestInterface $request, ResponseInterface $response) {
        return $request-&gt;getAttribute('route')-&gt;getMatches();
    }
));
</code></pre>
<h3 id=路由的不变性>路由的不变性</h3>
<p>所有路由规则都被设计为不可变对象，因此在运行中一旦路由规则已被创建，就不能再改变它们的状态，但可以创建副本并赋予新值。比如在构造函数之外给路由参数指定默认值：</p>
<pre><code class=language-php>namespace App\Bootloader;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $route = new Route('/[&lt;action&gt;]', function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        });

        $router-&gt;setRoute('home', $route-&gt;withDefaults([
            'action' =&gt; 'default'
        ]));
    }
}
</code></pre>
<h3 id=动词>动词</h3>
<p>使用 <code>withVerbs</code> 方法，可以让路由只匹配特定的 HTTP 动词（也称为“谓词”）：</p>
<pre><code class=language-php>namespace App\Bootloader;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $route = new Route('/[&lt;action&gt;]', function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        });

        $router-&gt;setRoute('get.route',
            $route-&gt;withVerbs('GET')-&gt;withDefaults(['action' =&gt; 'GET'])
        );

        $router-&gt;setRoute(
            'post.route',
            $route-&gt;withVerbs('POST', 'PUT')-&gt;withDefaults(['action' =&gt; 'POST'])
        );
    }
}
</code></pre>
<h3 id=中间件>中间件</h3>
<p>要把中间件关联到特定的路由，可以使用 <code>withMiddleware</code> 方法，对应的中间件中可以通过请求对象的 <code>route</code> 属性来获取路由参数的值：</p>
<pre><code class=language-php>namespace App\Bootloader;

use App\Middleware\ParamWatcher;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $route = new Route('/&lt;param&gt;', function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        });

        $router-&gt;setRoute('home', $route-&gt;withMiddleware(
            ParamWatcher::class
        ));
    }
}
</code></pre>
<p>示例中的 <code>ParamWatcher</code> 中间件代码如下：</p>
<pre><code class=language-php>namespace App\Middleware;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Http\Exception\ClientException\UnauthorizedException;
use Spiral\Router\RouteInterface;

class ParamWatcher implements MiddlewareInterface
{
    public function process(Request $request, RequestHandlerInterface $handler): Response
    {
        /** @var RouteInterface $route */
        $route = $request-&gt;getAttribute('route');

        if ($route-&gt;getMatches()['param'] === 'forbidden') {
           throw new UnauthorizedException();
        }

        return $handler-&gt;handle($request);
    }
}
</code></pre>
<p>上面的中间件在匹配到 <code>/forbidden</code> 的时候会抛出 <code>UnauthorizedException</code> 异常。</p>
<blockquote>
<p>根据需要，可以给路由关联任意数量的中间件。</p>
</blockquote>
<h2 id=多路由配置>多路由配置</h2>
<p>当有多条路由规则时，系统按照路由的定义顺序进行匹配，首先匹配到的路由生效。因此请避免先定义的路由规则包含后定义的路由规则的情况。</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'home',
    new Route('/&lt;param&gt;',
        function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        }
    )
);

// 下面的路由永远不会被触发
$router-&gt;setRoute(
    'hello',
    new Route('/hello',
        function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        }
    )
);
</code></pre>
<h2 id=默认路由>默认路由</h2>
<p>Spiral 路由组件支持定义默认（fallback）路由，也可称为“保底”路由。在所有其它路由都检查过且不匹配的情况下，默认路由总会被调用并检查请求是否与符合它的匹配模式。</p>
<p>比如，如果按照下面的方式定义默认路由之后，就不必为每个控制器和方法单独定义路由规则：</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'home',
    new Route('/&lt;param&gt;',
        function (ServerRequestInterface $request, ResponseInterface $response) {
            return $request-&gt;getAttribute('route')-&gt;getMatches();
        }
    )
);

$router-&gt;setDefault(new Route('/', function () {
    return 'default';
}));
</code></pre>
<p>所有没命中任何规则的路由，都会返回 &ldquo;default&rdquo; 字符串，而不是 404 错误。</p>
<p>参考下文，了解如何通过默认路由快速搭建应用程序的路径模式。</p>
<h2 id=路由目标控制器方法>路由目标（控制器方法）</h2>
<p>使用路由最高效的方法是把路由的目标指向控制器中的方法。为了演示所有功能，我们需要在 <code>App\Controller</code> 命名空间下定义多个控制器：</p>
<pre><code class=language-php>namespace App\Controller;

class HomeController
{
    public function index(): string
    {
        return 'index';
    }

    public function other(): string
    {
        return 'other';
    }

    public function user(int $id): string
    {
        return &quot;hello {$id}&quot;;
    }
}
</code></pre>
<p>通过脚手架命令 <code>php app.php create:controller demo -a test</code> 创建第二个控制器：</p>
<pre><code class=language-php>namespace App\Controller;

class DemoController
{
    public function test(): string
    {
        return 'demo test';
    }
}
</code></pre>
<h3 id=指向控制器方法>指向控制器方法</h3>
<p>要把路由的目标指向特定的控制器方法，可以使用 <code>Spiral\Router\Target\Action</code> 作为路由处理器：</p>
<pre><code class=language-php>namespace App\Bootloader;

use App\Controller\HomeController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Action;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute(
            'index',
            new Route('/index', new Action(HomeController::class, 'index'))
        );
    }
}
</code></pre>
<p>结合必须或可选控制器参数使用时，参数会以方法注入的方式传递给控制器方法：</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'user',
    new Route('/user/&lt;id:\d+&gt;', new Action(HomeController::class, 'user'))
);
</code></pre>
<h3 id=方法通配符>方法通配符</h3>
<p>在一条路由规则中，可以同时指定其匹配多个控制器方法，只要在路由匹配模式中增加一个 <code>&lt;action></code> 参数。由于要匹配的控制器方法中的某一个需要 <code>&lt;id></code> 参数，而另外的方法不需要，可以指定一个可选的 <code>&lt;id></code> 参数：</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'home',
    new Route('/&lt;action&gt;[/&lt;id&gt;]', new Action(HomeController::class, ['index', 'user']))
);
</code></pre>
<blockquote>
<p>上面的路由可以同时匹配 <code>/index</code> 和 <code>/user/</code>（注意：<code>/index/1</code> 和 <code>/user</code> 也会匹配）</p>
</blockquote>
<p>底层的实现中，上面的路由匹配模式会被编译为 <code>/^(?P&lt;action>index|user)(?:\/(?P&lt;id>[^\/]+))?$/iu</code>。这样的实现不只提升了性能，也让不同控制器方法重用相同的匹配模式。</p>
<pre><code class=language-php>// 匹配 &quot;/index&quot;
$router-&gt;setRoute(
    'home',
    new Route('/&lt;action&gt;', new Action(HomeController::class, 'index'))
);

// 匹配 &quot;/other&quot;
$router-&gt;setRoute(
    'home',
    new Route('/&lt;action&gt;', new Action(HomeController::class, 'other'))
);

// 匹配 &quot;/test&quot;
$router-&gt;setRoute(
    'demo',
    new Route('/&lt;action&gt;', new Action(DemoController::class, 'test'))
);
</code></pre>
<h3 id=指向控制器>指向控制器</h3>
<p>通过前面的例子，你会发现其实可以用一条路由规则匹配同一个控制器下的所有方法，但这种情况使用 <code>Spiral\Router\Target\Controller</code> 会更合适。以 <code>Controller</code> 为目标时，<code>&lt;action></code> 参数不能为空（除非为其指定了默认值）。</p>
<pre><code class=language-php>namespace App\Bootloader;

use App\Controller\HomeController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Controller;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute(
            'home',
            new Route('/home/&lt;action&gt;[/&lt;id&gt;]', new Controller(HomeController::class))
        );
    }
}
</code></pre>
<blockquote>
<p>上面的路由对 <code>/home/index</code>, <code>/home/other</code> 和 <code>/home/user/1</code> 都匹配。</p>
</blockquote>
<p>上面的路由规则配合路由参数默认值可以让 URL 更短。</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'home',
    (new Route('/home[/&lt;action&gt;[/&lt;id&gt;]]', new Controller(HomeController::class)))
        -&gt;withDefaults(['action' =&gt; 'index'])
);
</code></pre>
<blockquote>
<p>增加默认值之后，对于 <code>/home</code> 也可以匹配了，等同于 <code>/home/index</code>. 但要注意，用 <code>[]</code> 定义的路由匹配模式中的可选部分必须在匹配模式的最后，不能在必须参数之前。</p>
</blockquote>
<h3 id=指向命名空间>指向命名空间</h3>
<p>有时候，你可能想让一个路由规则同时匹配相同命名空间下的多个控制器。这种情况可以使用 <code>Spiral\Router\Target\Namespaced</code> 来实现。以 <code>Namespaced</code> 作为目标的路由匹配模式必须指定 <code>&lt;controller></code> 和 <code>&lt;action></code> 参数（除非为其指定了默认值）。</p>
<p>定义这样的路由规则时，可以指定目标命名空间和控制器类名后缀：</p>
<pre><code class=language-php>namespace App\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Namespaced;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute('app', new Route(
            '/&lt;controller&gt;/&lt;action&gt;',
            new Namespaced('App\Controller', 'Controller')
        ));
    }
}
</code></pre>
<blockquote>
<p>这条路由规则可以匹配 <code>/home/index</code>, <code>/home/other</code> 和 <code>/demo/test</code>.</p>
</blockquote>
<p>如果指定了 <code>controller</code> 和 <code>action</code> 参数的默认值，那么 URL 中就可以省略它们：</p>
<pre><code class=language-php>$router-&gt;setRoute('app',
    (new Route(
        '[/&lt;controller&gt;[/&lt;action&gt;]]',
        new Namespaced('App\Controller', 'Controller')
    ))-&gt;withDefaults([
        'controller' =&gt; 'home',
        'action'     =&gt; 'index'
    ])
);
</code></pre>
<blockquote>
<p>改造后的路由可以匹配 <code>/</code>（等于 <code>/home/index</code>）、<code>/home</code>（等于 <code>/home/index</code>）、<code>/home/index</code>、<code>/home/other</code> 和 <code>/demo/test</code>. 但访问 <code>/demo</code> 会触发 404 错误，因为 <code>DemoController</code> 没有定义默认的 <code>index</code> 方法。</p>
</blockquote>
<p>Spiral 的 Web 应用框架默认已经定义了上面这条路由规则并将其
<a href=https://github.com/spiral/app/blob/master/app/src/Bootloader/RoutesBootloader.php#L42 target=_blank rel=noopener>作为默认路由</a>。因此你不必为 <code>App\Controller</code> 命名空间下的控制器和方法创建任何路由，只要使用 <code>/controller/action</code> 这种形式的 URL 就能访问到对应的方法。如果没有指定方法名，那么 <code>index</code> 方法会被默认调用，如果没有指定控制器，那么 <code>HomeController</code> 会被默认调用。还有一点要说明的是，只有控制器中的访问级别为 <code>public</code> 的方法才会被路由调用。</p>
<blockquote>
<p>在开发完成之后，你可以考虑把默认路由关闭。</p>
</blockquote>
<h3 id=指向控制器组>指向控制器组</h3>
<p>一条路由规则同时匹配多个控制器还有另一个替代方法，可以手动指定要匹配的控制器而不是公共命名空间。这种情况需要使用 <code>Spiral\Router\Target\Group</code> 作为路由目标，同样必须指定 <code>&lt;Controller></code> 和 <code>&lt;action></code> 参数（除非为它们提供默认值）。</p>
<pre><code class=language-php>namespace App\Bootloader;

use App\Controller\DemoController;
use App\Controller\HomeController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Group;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $router-&gt;setRoute('app', new Route('/&lt;controller&gt;/&lt;action&gt;', new Group([
            'home' =&gt; HomeController::class,
            'demo' =&gt; DemoController::class
        ])));
    }
}
</code></pre>
<blockquote>
<p>在需要让一级路径匹配多个模块（比如公共页面和管理面板）的时候，这种方法会很有用，因为通常它们会位于不同的命名空间下。</p>
</blockquote>
<h2 id=restful>RESTful</h2>
<p>上面介绍过的所有路由目标都支持第三个参数，这个参数规定了系统选择目标方法的行为。如果将这个参数值指定为 <code>TargetInterface::RESTFUL</code>，那么系统在调用对应的方法函数时，会自动在方法名称前面加上 HTTP 动词，比如 <code>index</code> 会变成 <code>getIndex</code> 或者 <code>postIndex</code>.</p>
<p>举个例子，假如有如下的控制器：</p>
<pre><code class=language-php>namespace App\Controller;

class UserController
{
    public function getUser(int $id): string
    {
        return &quot;get {$id}&quot;;
    }

    public function postUser(int $id): string
    {
        return &quot;post {$id}&quot;;
    }

    public function deleteUser(int $id): string
    {
        return &quot;delete {$id}&quot;;
    }
}
</code></pre>
<p>然后为它定义了这样的路由规则：</p>
<pre><code class=language-php>$router-&gt;setRoute('user', new Route(
    '/user/&lt;id:\d+&gt;',
    new Controller(UserController::class, Controller::RESTFUL),
    ['action' =&gt; 'user']
));
</code></pre>
<blockquote>
<p>在用不同的 HTTP 动词访问 <code>/user/1</code> 的时候，会调用不同的控制器方法。注意：你还是要指定方法名称（例子中的 <code>user</code>）。</p>
</blockquote>
<h3 id=跨路由共享目标>跨路由共享目标</h3>
<p>另一个定义 RESTful 路由，或者说给多个控制器定义相同的路由规则的方法是让不同的路由共享相同的路由目标。这种方法要求你的控制器方法名要采用相同的风格。</p>
<p>举个例子，我们可以把不同的 HTTP 动词路由到下面这种命名风格的控制器：</p>
<pre><code class=language-php>namespace App\Controller;

class UserController
{
    public function load(int $id): string
    {
        return &quot;get {$id}&quot;;
    }

    public function store(int $id): string
    {
        return &quot;post {$id}&quot;;
    }

    public function delete(int $id): string
    {
        return &quot;delete {$id}&quot;;
    }
}
</code></pre>
<p>我们可以创建形如 <code>GET|POST|DELETE /v1/&lt;controller></code> 的 API, 该 API 会被指向正确的控制器方法。</p>
<p>基础的路由规则看起来类似这样：</p>
<pre><code class=language-php>$resource = new Route('/v1/&lt;controller&gt;', new Group([
    'user' =&gt; UserController::class,
]));
</code></pre>
<p>然后我们把这个基础的路由注册到不同的 HTTP 动词和控制器方法：</p>
<pre><code class=language-php>namespace App\Bootloader;

use App\Controller\UserController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Group;

class RoutesBootloader extends Bootloader
{
    public function boot(RouterInterface $router)
    {
        $resource = new Route('/v1/&lt;controller&gt;/&lt;id&gt;', new Group([
            'user' =&gt; UserController::class,
            // 'book' =&gt; BookController::class,
            // 'order' =&gt; OrderController::class,
        ]));

        $router-&gt;setRoute(
            'resource.get',
            $resource-&gt;withVerbs('GET')-&gt;withDefaults(['action' =&gt; 'load'])
        );

        $router-&gt;setRoute(
            'resource.store',
            $resource-&gt;withVerbs('POST')-&gt;withDefaults(['action' =&gt; 'store'])
        );

        $router-&gt;setRoute(
            'resource.delete',
            $resource-&gt;withVerbs('DELETE')-&gt;withDefaults(['action' =&gt; 'delete'])
        );
    }
}
</code></pre>
<p>这样就可以给多个资源控制器使用相同的一组路由规则。</p>
<h2 id=url-生成>URL 生成</h2>
<p>路由除了用来匹配用户访问的 URL，也可以根据给出的路由和参数生成正确的 URL.</p>
<pre><code class=language-php>$router-&gt;setRoute(
    'home',
    new Route('/home/&lt;action&gt;', new Controller(HomeController::class))
);
</code></pre>
<p>对于上面的路由规则，使用 <code>RouterInterface</code> 的 <code>uri</code> 方法可以生成正确的 URL:</p>
<pre><code class=language-php>use Spiral\Router\RouterInterface;

// ...

public function index(RouterInterface $router)
{
    $uri = $router-&gt;uri('home', ['action' =&gt; 'index']);

    dump((string)$uri); // /home/index
}
</code></pre>
<p>传入额外的（未在路由匹配模式中定义的）参数，会自动添加到查询字符串：</p>
<pre><code class=language-php>use Spiral\Router\RouterInterface;

// ...

public function index(RouterInterface $router)
{
        $uri = $router-&gt;uri('home', [
        'action' =&gt; 'index',
        'page'   =&gt; 123
    ]);

    dump((string)$uri); // /home/index?page=123
}
</code></pre>
<p><code>uri</code> 方法返回的值是 <code>Psr\Http\Message\UriInterface</code> 的实例而不是字符串：</p>
<pre><code class=language-php>use Spiral\Router\RouterInterface;

// ...

public function index(RouterInterface $router)
{
    $uri = $router-&gt;uri('home', [
        'action' =&gt; 'index',
        'page'   =&gt; 123
    ]);

    dump((string)$uri-&gt;withFragment('hello')); // /home/index?page=123#hello
}
</code></pre>
<blockquote>
<p>注意：所有传递给 URL 匹配模式的参数都会 slug 化（空格转为 <code>-</code>，大写转为小写）：</p>
</blockquote>
<pre><code class=language-php>use Spiral\Router\RouterInterface;

// ...

public function index(RouterInterface $router)
{
    $uri = $router-&gt;uri('home', [
        'action' =&gt; 'hello World',
    ]);

    dump((string)$uri); // /home/hello-world
}
</code></pre>
<blockquote>
<p>在 Stempler 模板引擎中，可以使用 <code>@route(name, params)</code> 指定来生成 URL.</p>
</blockquote>
</div>
<div class=article-widget>
<div class=post-nav>
<div class=post-nav-item>
<div class=meta-nav>上一页</div>
<a href=/docs/http/request-response/ rel=next>请求和响应</a>
</div>
<div class=post-nav-item>
<div class=meta-nav>下一页</div>
<a href=/docs/basics/quick-start/ rel=prev>上手指南</a>
</div>
</div>
</div>
</div>
<div class=body-footer>
<p>最近更新于 2020-08-28</p>
<p class=edit-page>
<a href=https://github.com/krwu/spiraldocs/edit/feat/chinese/zh_CN/http/routing.md>
<i class="fas fa-pen pr-2"></i>编辑本页
</a>
</p>
</div>
</article>
<footer class=site-footer>
<p class=powered-by><span class=copyright>© 2019 - 2021 <a href=https://studyspiral.cn/>StudySpiral</a> All rights reserved.</span>
</p>
<p class=powered-by>
<a href=http://www.beian.miit.gov.cn/ target=_blank rel="noopener noreferer">粤ICP备14011364号</a>
<a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44030402001866" target=_blank rel="noopener noreferer"><img style=display:inline-block src=/img/gaba.png>粤公网安备 44030402001866号</a>
</p>
<p id=webify class=powered-by style=display:flex><span style="margin:0 .2rem 0 0;display:block;height:20px;line-height:20px">Powered by</span> <a href=https://webify.cloudbase.net/ target=_blank rel=noopener>CloudBase Webify</a></p>
<script async defer>window.addEventListener('DOMContentLoaded',()=>{const{host:a}=window.location;(a.toLocaleLowerCase()==='docs.studyspiral.cn'||a.indexOf(".app.tcloudbase.com"))&&(document.getElementById('webify').style.display='flex')})</script>
</footer>
</main>
</div>
</div>
<script src=https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js integrity="sha256-lqvxZrPLtfffUl2G/e7szqSvPBILGbwmsGE1MKlOi0Q=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/jquery.isotope/3.0.6/isotope.pkgd.min.js integrity="sha256-CBrpuqrMhXwcLLUd5tvQ4euBHCdh7wGlDfNz8vbu/iI=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js integrity="sha256-yt2kYMy0w8AbtF89WXb2P1rfjcP/HTHLT7097U8Y5b8=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.1/highlight.min.js integrity="sha256-eOgo0OtLL4cdq7RdwRUiGKLX9XsIJ7nGhWEKbohmVAQ=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.1/languages/go.min.js></script>
<script>const code_highlighting=!0</script>
<script>const isSiteThemeDark=!1</script>
<script>const search_config={indexURI:"/index.json",minLength:1,threshold:.3},i18n={no_results:"没有找到结果",placeholder:"搜索...",results:"搜索结果"},content_type={post:"文章",project:"项目",publication:"出版物",talk:"演讲"}</script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.1.1/anchor.min.js integrity="sha256-pB/deHc9CGfFpJRjC43imB29Rse8tak+5eXqntO94ck=" crossorigin=anonymous></script>
<script>anchors.add()</script>
<script id=search-hit-fuse-template type=text/x-template>
      <div class="search-hit" id="summary-{{key}}">
      <div class="search-hit-content">
        <div class="search-hit-name">
          <a href="{{relpermalink}}">{{title}}</a>
          <div class="article-metadata search-hit-type">{{type}}</div>
          <p class="search-hit-description">{{snippet}}</p>
        </div>
      </div>
      </div>
    </script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.1/fuse.min.js integrity="sha256-VzgmKYmhsGNNN4Ph1kMW+BjoYJM2jV5i4IlFoeZA9XI=" crossorigin=anonymous></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/jquery.mark.min.js integrity="sha256-4HLtjeVgH0eIB3aZ9mLYF6E8oU5chNdjU6p6rrXpl9U=" crossorigin=anonymous></script>
<script src=/js/academic.min.6f1005d1a84220e2f466ff3d8e712f31.js></script>
<div id=modal class="modal fade" role=dialog>
<div class=modal-dialog>
<div class=modal-content>
<div class=modal-header>
<h5 class=modal-title>引用</h5>
<button type=button class=close data-dismiss=modal aria-label=Close>
<span aria-hidden=true>&#215;</span>
</button>
</div>
<div class=modal-body>
<pre><code class="tex hljs"></code></pre>
</div>
<div class=modal-footer>
<a class="btn btn-outline-primary my-1 js-copy-cite" href=# target=_blank>
<i class="fas fa-copy"></i> 复制
</a>
<a class="btn btn-outline-primary my-1 js-download-cite" href=# target=_blank>
<i class="fas fa-download"></i> 下载
</a>
<div id=modal-error></div>
</div>
</div>
</div>
</div>
</body>
</html>